#
# peerscape.db.sqlite_db
#
# Copyright 2003-2009 Helsinki Institute for Information Technology
# and the authors.
#
# Authors: Ken Rimey <rimey@hiit.fi>
#

# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

# This file derives from db.py in PDIS (http://pdis.hiit.fi/).

"""
Base class for database handles based on pysqlite
"""

from __future__ import with_statement
from contextlib import contextmanager

import os
import sys
import logging

import sqlite3

Error = sqlite3.Error
DatabaseError = sqlite3.DatabaseError
IntegrityError = sqlite3.IntegrityError

DEBUG = False

class SqliteDB(object):
    sqlite_wrapper = 'sqlite3'

    # Statements to execute when initializing a new database:
    initialization = []

    def __init__(self, path=':memory:', create=True, timeout=None):
        if timeout is None:
            if sys.version < '2.6':
                # We have experienced deadlocks with Python 2.5 due
                # to Issue #3846 (fixed in Python 2.6).  Reducing the
                # timeout makes these deadlocks less painful.
                timeout = 5     # Python 2.5
            else:
                timeout = 30    # Python 2.6

        self.path = os.path.normpath(os.path.expanduser(path))
        if create:
            dir = os.path.dirname(self.path)
            if dir and not os.path.exists(dir):
                os.makedirs(dir)

        if hasattr(sqlite3, 'enable_shared_cache'):
            # Clear shared cache mode in case somebody (XULRunner) has set it:
            sqlite3.enable_shared_cache(False)

        self.connection = sqlite3.Connection(self.path,
                                             timeout=timeout,
                                             isolation_level=None)
        self.nesting = 0
        self.strategy = None

        # Default is FULL, which is supposedly slow.  However, the
        # following seems to cause problems where the database is left
        # locked.
        #self.execute("pragma synchronous = NORMAL")

        if create:
            # This pragma has no effect if I execute it in a transaction.
            self.execute("pragma auto_vacuum=1")

            # Using execute() here instead of operate() makes opening the
            # database twice as fast, and avoids locking it.
            for statement in self.initialization:
                try:
                    self.execute(statement)
                except DatabaseError:
                    # This is a hack for Mac OS X 10.4, which comes
                    # with sqlite 3.1.3.  The "if not exists" clause
                    # was not added until 3.3.0 alpha.
                    statement = statement.replace(' if not exists', '')
                    try:
                        self.execute(statement)
                    except DatabaseError:
                        pass    # The table already exists.

    def close(self):
        self.connection.close()

    #
    # Database helpers
    #

    def read_transaction(self):
        return self.transaction()

    def write_transaction(self):
        return self.transaction('immediate')

    @contextmanager
    def transaction(self, strategy=None):
        self.begin(strategy)
        try:
            yield
        except:
            self.rollback()
            raise
        else:
            self.commit()

    def begin(self, strategy=None):
        if self.nesting == 0:
            assert self.strategy is None
            if strategy is None:
                self.execute("begin")
            else:
                self.execute("begin %s" % strategy)
                self.strategy = strategy
        elif strategy is not None:
            assert strategy == self.strategy

        self.nesting += 1

    def commit(self):
        self.nesting -= 1
        if self.nesting == 0:
            self.strategy = None
            try:
                self.execute("commit")
            except:
                self.execute("rollback") # XXX Ignore exceptions?
                raise

    def rollback(self):
        self.nesting -= 1
        if self.nesting == 0:
            self.strategy = None
            self.execute("rollback") # XXX Ignore exceptions?

    def execute(self, statement, bindings=()):
        if self.nesting == 0 and DEBUG:
            logging.debug('Executing "%s"...', statement)
        cursor = self.connection.execute(statement, bindings)
        if DEBUG:
            logging.debug('Executed SQL: %s%s', "  " * self.nesting, statement)
        return cursor

    def operate(self, statement, bindings=()):
        with self.write_transaction():
            return self.execute(statement, bindings)
